/*
 *    Copyright 2010 The maven-gcu-plugin Team
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.googlecode.gcu;

import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.List;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
import org.codehaus.plexus.util.Base64;
import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher;
import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException;

/**
 * Goal which uploads artifacts to Google Code.
 *
 * @goal upload
 * @phase deploy
 * @requiresOnline true
 * @version $Id: UploadMojo.java 68 2010-08-29 19:57:12Z simone.tripodi $
 */
public final class UploadMojo extends AbstractMojo {

    private static final String UPLOAD_URL_PATTERN = "https://%s.googlecode.com/files";

    private static final String AUTHORIZATION = "Authorization";

    private static final String BASIC_PATTERN = "Basic %s";

    private static final String CONTENT_TYPE = "Content-Type";

    private static final String CONTENT_TYPE_MULTIPART = "multipart/form-data; boundary=%s";

    private static final String USER_AGENT = "User-Agent";

    private static final String USER_AGENT_NAME = "Maven Google-Code Upload Plugin";

    private static final String SUMMARY = "content-disposition: form-data; name=\"summary\"";

    private static final String LABEL = "content-disposition: form-data; name=\"label\"";

    private static final String FILE_NAME = "content-disposition: form-data; name=\"filename\"; filename=\"%s\"";

    private static final String OCTET_STREAM = "Content-Type: application/octet-stream";

    private static final String EMPTY_STRING = "";

    private static final String ASCII = "ascii";

    private static final String CRLF = "\r\n";

    private static final Charset UTF_8 = Charset.forName("UTF-8");

    /**
     * Access to settings.
     *
     * @parameter expression="${settings}"
     * @readonly
     */
    private Settings settings;

    /**
     * The server id (corresponding to a server's id in settings.xml).
     *
     * @parameter default-value="googlecode"
     */
    private String serverId;

    /**
     * Google Code project name to upload to.
     *
     * @parameter expression="${project.artifactId}"
     */
    private String projectName;

    /**
     * The list of {@link Upload} have to be performed.
     *
     * @parameter
     * @required
     */
    private List<Upload> uploads;

    /**
     * If true, the plugin fails when an error occurs, just log it otherwise.
     *
     * @parameter default-value="false"
     */
    private boolean failsOnError;

    /**
     * The security dispatcher to decrypt passwords.
     *
     * @component role="hidden.org.sonatype.plexus.components.sec.dispatcher.SecDispatcher"
     * @required
     */
    private SecDispatcher securityDispatcher;

    /**
     * {@inheritDoc}
     */
    public void execute() throws MojoExecutionException {
        if (this.uploads.isEmpty()) {
            String errorMessage = "No uploads specified!";

            if (this.failsOnError) {
                throw new MojoExecutionException(errorMessage);
            } else if (this.getLog().isWarnEnabled()) {
                this.getLog().warn(errorMessage);
            }
            return;
        }

        Server server = this.settings.getServer(this.serverId);
        if (server == null) {
            String errorMessage = "Server with id '"
                + this.serverId
                + "' not found, please make sure it is present in the ~/.m2/settings.xml file";

            if (this.failsOnError) {
                throw new MojoExecutionException(errorMessage);
            } else {
                this.getLog().error(errorMessage);
                return;
            }
        }

        String username = server.getUsername();
        String password;
        try {
            password = this.securityDispatcher.decrypt(server.getPassword());
        } catch (SecDispatcherException e) {
            throw new MojoExecutionException("An error occurred while decrypting password of server "
                    + this.serverId
                    + ", see nested exception", e);
        }

        if (this.getLog().isInfoEnabled()) {
            this.getLog().info("Accessing to server '"
                    + this.serverId
                    + "' using username '"
                    + username
                    + "' and password '"
                    + password.replaceAll("\\w", "*")
                    + "'");
        }

        String authToken = null;
        URL service = null;
        try {
            authToken = String.format(BASIC_PATTERN,
                    new String(Base64.encodeBase64((username + ':' + password).getBytes(UTF_8.name()))));
            service = new URL(String.format(UPLOAD_URL_PATTERN, this.projectName));
        } catch (Exception e) {
            // TODO should not happen
            throw new MojoExecutionException("Impossible to decode the user password", e);
        }

        for (Upload upload : this.uploads) {
            if (this.getLog().isInfoEnabled()) {
                this.getLog().info("Uploading "
                        + upload
                        + " on "
                        + service);
            }

            HttpURLConnection conn = null;
            OutputStream out = null;
            InputStream in = null;
            try {
                conn = (HttpURLConnection) service.openConnection();
                conn.setDoOutput(true);
                conn.setRequestProperty(AUTHORIZATION, authToken);
                conn.setRequestProperty(CONTENT_TYPE, String.format(CONTENT_TYPE_MULTIPART, this.getClass().getName()));
                conn.setRequestProperty(USER_AGENT, USER_AGENT_NAME);

                out = conn.getOutputStream();
                sendLine(out, "--" + this.getClass().getName());
                sendLine(out, SUMMARY);
                sendLine(out, EMPTY_STRING);
                sendLine(out, upload.getSummary());

                for (String label : upload.getLabels()) {
                    sendLine(out, "--" + this.getClass().getName());
                    sendLine(out, LABEL);
                    sendLine(out, EMPTY_STRING);
                    sendLine(out, label);
                }

                sendLine(out, "--" + this.getClass().getName());
                sendLine(out, String.format(FILE_NAME, upload.getFile().getAbsolutePath()));
                sendLine(out, OCTET_STREAM);
                sendLine(out, EMPTY_STRING);

                in = new BufferedInputStream(new FileInputStream(upload.getFile()));

                int count;
                int progressCount = 0;
                byte[] buf = new byte[8192];
                while ((count = in.read(buf)) >= 0) {
                    out.write(buf, 0, count);

                    progressCount += count;
                    System.out.print("\r" + ((progressCount * 100) / upload.getFile().length()) + "%");
                }
                System.out.println("\rDone!");

                sendLine(out, EMPTY_STRING);
                sendLine(out, "--" + this.getClass().getName() + "--");

                if (conn.getResponseCode() != HttpURLConnection.HTTP_CREATED) {
                    String errorMessage = "Impossible to upload "
                        + upload
                        + " on "
                        + service
                        + ", server replied "
                        + conn.getResponseMessage();
                    if (this.failsOnError) {
                        throw new MojoExecutionException(errorMessage);
                    } else {
                        this.getLog().error(errorMessage);
                    }
                }
            }  catch (IOException e) {
                String errorMessage = "Impossible to perform upload "
                    + upload
                    + " on "
                    + service;

                if (this.failsOnError) {
                    throw new MojoExecutionException(errorMessage, e);
                } else {
                    getLog().error(errorMessage, e);
                }
            } finally {
                closeQuietly(in);
                closeQuietly(out);

                if (conn != null) {
                    conn.disconnect();
                }
            }
        }
    }

    /**
     * Writes a string to an output stream using the ASCII encoding.
     *
     * @param target the OutputStream where string has to be written.
     * @param string the string has to be written.
     * @throws IOException if any error occurs while writing the string.
     */
    private static void sendLine(OutputStream target, String string) throws IOException {
        target.write(string.getBytes(ASCII));
        target.write(CRLF.getBytes(ASCII));
    }

    /**
     * Close quietly a {@link Closeable} resource.
     *
     * @param closeable the resource has to be closed.
     */
    private static void closeQuietly(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                // close quietly
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "{ settings="
                + this.settings
                + ", serverId="
                + this.serverId
                + ", projectName="
                + this.projectName
                + ", uploads="
                + this.uploads
                + ", failsOnError="
                + this.failsOnError
                + " }";
    }

}
